// Copyright (c) 1999-2010 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.math.*;
import java.security.*;
import java.security.interfaces.*;
import java.security.spec.*;
import java.util.*;

/**
 * Constants and methods relating to DNSSEC.
 * 
 * DNSSEC provides authentication for DNS information.
 * 
 * @see RRSIGRecord
 * @see DNSKEYRecord
 * @see RRset
 * 
 * @author Brian Wellington
 */
@SuppressWarnings("rawtypes")
public class DNSSEC {

	public static class Algorithm {
		private Algorithm() {
		}

		/** RSA/MD5 public key (deprecated) */
		public static final int RSAMD5 = 1;

		/** Diffie Hellman key */
		public static final int DH = 2;

		/** DSA public key */
		public static final int DSA = 3;

		/** RSA/SHA1 public key */
		public static final int RSASHA1 = 5;

		/** DSA/SHA1, NSEC3-aware public key */
		public static final int DSA_NSEC3_SHA1 = 6;

		/** RSA/SHA1, NSEC3-aware public key */
		public static final int RSA_NSEC3_SHA1 = 7;

		/** RSA/SHA256 public key */
		public static final int RSASHA256 = 8;

		/** RSA/SHA512 public key */
		public static final int RSASHA512 = 10;

		/** ECDSA Curve P-256 with SHA-256 public key **/
		public static final int ECDSAP256SHA256 = 13;

		/** ECDSA Curve P-384 with SHA-384 public key **/
		public static final int ECDSAP384SHA384 = 14;

		/** Indirect keys; the actual key is elsewhere. */
		public static final int INDIRECT = 252;

		/** Private algorithm, specified by domain name */
		public static final int PRIVATEDNS = 253;

		/** Private algorithm, specified by OID */
		public static final int PRIVATEOID = 254;

		private static Mnemonic algs = new Mnemonic("DNSSEC algorithm",
				Mnemonic.CASE_UPPER);

		static {
			algs.setMaximum(0xFF);
			algs.setNumericAllowed(true);

			algs.add(RSAMD5, "RSAMD5");
			algs.add(DH, "DH");
			algs.add(DSA, "DSA");
			algs.add(RSASHA1, "RSASHA1");
			algs.add(DSA_NSEC3_SHA1, "DSA-NSEC3-SHA1");
			algs.add(RSA_NSEC3_SHA1, "RSA-NSEC3-SHA1");
			algs.add(RSASHA256, "RSASHA256");
			algs.add(RSASHA512, "RSASHA512");
			algs.add(ECDSAP256SHA256, "ECDSAP256SHA256");
			algs.add(ECDSAP384SHA384, "ECDSAP384SHA384");
			algs.add(INDIRECT, "INDIRECT");
			algs.add(PRIVATEDNS, "PRIVATEDNS");
			algs.add(PRIVATEOID, "PRIVATEOID");
		}

		/**
		 * Converts an algorithm into its textual representation
		 */
		public static String string(int alg) {
			return algs.getText(alg);
		}

		/**
		 * Converts a textual representation of an algorithm into its numeric
		 * code. Integers in the range 0..255 are also accepted.
		 * 
		 * @param s
		 *            The textual representation of the algorithm
		 * @return The algorithm code, or -1 on error.
		 */
		public static int value(String s) {
			return algs.getValue(s);
		}
	}

	private DNSSEC() {
	}

	private static void digestSIG(DNSOutput out, SIGBase sig) {
		out.writeU16(sig.getTypeCovered());
		out.writeU8(sig.getAlgorithm());
		out.writeU8(sig.getLabels());
		out.writeU32(sig.getOrigTTL());
		out.writeU32(sig.getExpire().getTime() / 1000);
		out.writeU32(sig.getTimeSigned().getTime() / 1000);
		out.writeU16(sig.getFootprint());
		sig.getSigner().toWireCanonical(out);
	}

	/**
	 * Creates a byte array containing the concatenation of the fields of the
	 * SIG record and the RRsets to be signed/verified. This does not perform a
	 * cryptographic digest.
	 * 
	 * @param rrsig
	 *            The RRSIG record used to sign/verify the rrset.
	 * @param rrset
	 *            The data to be signed/verified.
	 * @return The data to be cryptographically signed or verified.
	 */
	
	public static byte[] digestRRset(RRSIGRecord rrsig, RRset rrset) {
		DNSOutput out = new DNSOutput();
		digestSIG(out, rrsig);

		int size = rrset.size();
		Record[] records = new Record[size];

		Iterator it = rrset.rrs();
		Name name = rrset.getName();
		Name wild = null;
		int sigLabels = rrsig.getLabels() + 1; // Add the root label back.
		if (name.labels() > sigLabels)
			wild = name.wild(name.labels() - sigLabels);
		while (it.hasNext())
			records[--size] = (Record) it.next();
		Arrays.sort(records);

		DNSOutput header = new DNSOutput();
		if (wild != null)
			wild.toWireCanonical(header);
		else
			name.toWireCanonical(header);
		header.writeU16(rrset.getType());
		header.writeU16(rrset.getDClass());
		header.writeU32(rrsig.getOrigTTL());
		for (int i = 0; i < records.length; i++) {
			out.writeByteArray(header.toByteArray());
			int lengthPosition = out.current();
			out.writeU16(0);
			out.writeByteArray(records[i].rdataToWireCanonical());
			int rrlength = out.current() - lengthPosition - 2;
			out.save();
			out.jump(lengthPosition);
			out.writeU16(rrlength);
			out.restore();
		}
		return out.toByteArray();
	}

	/**
	 * Creates a byte array containing the concatenation of the fields of the
	 * SIG(0) record and the message to be signed. This does not perform a
	 * cryptographic digest.
	 * 
	 * @param sig
	 *            The SIG record used to sign the rrset.
	 * @param msg
	 *            The message to be signed.
	 * @param previous
	 *            If this is a response, the signature from the query.
	 * @return The data to be cryptographically signed.
	 */
	public static byte[] digestMessage(SIGRecord sig, Message msg,
			byte[] previous) {
		DNSOutput out = new DNSOutput();
		digestSIG(out, sig);

		if (previous != null)
			out.writeByteArray(previous);

		msg.toWire(out);
		return out.toByteArray();
	}

	/**
	 * A DNSSEC exception.
	 */
	@SuppressWarnings("serial")
	public static class DNSSECException extends Exception {
		DNSSECException(String s) {
			super(s);
		}
	}

	/**
	 * An algorithm is unsupported by this DNSSEC implementation.
	 */
	@SuppressWarnings("serial")
	public static class UnsupportedAlgorithmException extends DNSSECException {
		UnsupportedAlgorithmException(int alg) {
			super("Unsupported algorithm: " + alg);
		}
	}

	/**
	 * The cryptographic data in a DNSSEC key is malformed.
	 */
	@SuppressWarnings("serial")
	public static class MalformedKeyException extends DNSSECException {
		MalformedKeyException(KEYBase rec) {
			super("Invalid key data: " + rec.rdataToString());
		}
	}

	/**
	 * A DNSSEC verification failed because fields in the DNSKEY and RRSIG
	 * records do not match.
	 */
	@SuppressWarnings("serial")
	public static class KeyMismatchException extends DNSSECException {
		@SuppressWarnings("unused")
		private KEYBase key;
		@SuppressWarnings("unused")
		private SIGBase sig;

		KeyMismatchException(KEYBase key, SIGBase sig) {
			super("key " + key.getName() + "/"
					+ DNSSEC.Algorithm.string(key.getAlgorithm()) + "/"
					+ key.getFootprint() + " " + "does not match signature "
					+ sig.getSigner() + "/"
					+ DNSSEC.Algorithm.string(sig.getAlgorithm()) + "/"
					+ sig.getFootprint());
		}
	}

	/**
	 * A DNSSEC verification failed because the signature has expired.
	 */
	@SuppressWarnings("serial")
	public static class SignatureExpiredException extends DNSSECException {
		private Date when, now;

		SignatureExpiredException(Date when, Date now) {
			super("signature expired");
			this.when = when;
			this.now = now;
		}

		/**
		 * @return When the signature expired
		 */
		public Date getExpiration() {
			return when;
		}

		/**
		 * @return When the verification was attempted
		 */
		public Date getVerifyTime() {
			return now;
		}
	}

	/**
	 * A DNSSEC verification failed because the signature has not yet become
	 * valid.
	 */
	@SuppressWarnings("serial")
	public static class SignatureNotYetValidException extends DNSSECException {
		private Date when, now;

		SignatureNotYetValidException(Date when, Date now) {
			super("signature is not yet valid");
			this.when = when;
			this.now = now;
		}

		/**
		 * @return When the signature will become valid
		 */
		public Date getExpiration() {
			return when;
		}

		/**
		 * @return When the verification was attempted
		 */
		public Date getVerifyTime() {
			return now;
		}
	}

	/**
	 * A DNSSEC verification failed because the cryptographic signature
	 * verification failed.
	 */
	@SuppressWarnings("serial")
	public static class SignatureVerificationException extends DNSSECException {
		SignatureVerificationException() {
			super("signature verification failed");
		}
	}

	/**
	 * The key data provided is inconsistent.
	 */
	@SuppressWarnings("serial")
	public static class IncompatibleKeyException extends
			IllegalArgumentException {
		IncompatibleKeyException() {
			super("incompatible keys");
		}
	}

	private static int BigIntegerLength(BigInteger i) {
		return (i.bitLength() + 7) / 8;
	}

	private static BigInteger readBigInteger(DNSInput in, int len)
			throws IOException {
		byte[] b = in.readByteArray(len);
		return new BigInteger(1, b);
	}

	private static BigInteger readBigInteger(DNSInput in) {
		byte[] b = in.readByteArray();
		return new BigInteger(1, b);
	}

	private static void writeBigInteger(DNSOutput out, BigInteger val) {
		byte[] b = val.toByteArray();
		if (b[0] == 0)
			out.writeByteArray(b, 1, b.length - 1);
		else
			out.writeByteArray(b);
	}

	private static PublicKey toRSAPublicKey(KEYBase r) throws IOException,
			GeneralSecurityException {
		DNSInput in = new DNSInput(r.getKey());
		int exponentLength = in.readU8();
		if (exponentLength == 0)
			exponentLength = in.readU16();
		BigInteger exponent = readBigInteger(in, exponentLength);
		BigInteger modulus = readBigInteger(in);

		KeyFactory factory = KeyFactory.getInstance("RSA");
		return factory.generatePublic(new RSAPublicKeySpec(modulus, exponent));
	}

	private static PublicKey toDSAPublicKey(KEYBase r) throws IOException,
			GeneralSecurityException, MalformedKeyException {
		DNSInput in = new DNSInput(r.getKey());

		int t = in.readU8();
		if (t > 8)
			throw new MalformedKeyException(r);

		BigInteger q = readBigInteger(in, 20);
		BigInteger p = readBigInteger(in, 64 + t * 8);
		BigInteger g = readBigInteger(in, 64 + t * 8);
		BigInteger y = readBigInteger(in, 64 + t * 8);

		KeyFactory factory = KeyFactory.getInstance("DSA");
		return factory.generatePublic(new DSAPublicKeySpec(y, p, q, g));
	}

	private static class ECKeyInfo {
		int length;
		public BigInteger p, a, b, gx, gy, n;
		EllipticCurve curve;
		ECParameterSpec spec;

		ECKeyInfo(int length, String p_str, String a_str, String b_str,
				String gx_str, String gy_str, String n_str) {
			this.length = length;
			p = new BigInteger(p_str, 16);
			a = new BigInteger(a_str, 16);
			b = new BigInteger(b_str, 16);
			gx = new BigInteger(gx_str, 16);
			gy = new BigInteger(gy_str, 16);
			n = new BigInteger(n_str, 16);
			curve = new EllipticCurve(new ECFieldFp(p), a, b);
			spec = new ECParameterSpec(curve, new ECPoint(gx, gy), n, 1);
		}
	}

	// RFC 5114 Section 2.6
	private static final ECKeyInfo ECDSA_P256 = new ECKeyInfo(32,
			"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
			"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
			"5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B",
			"6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
			"4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
			"FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");

	// RFC 5114 Section 2.7
	private static final ECKeyInfo ECDSA_P384 = new ECKeyInfo(
			48,
			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF",
			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC",
			"B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF",
			"AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7",
			"3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F",
			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973");

	private static PublicKey toECDSAPublicKey(KEYBase r, ECKeyInfo keyinfo)
			throws IOException, GeneralSecurityException, MalformedKeyException {
		DNSInput in = new DNSInput(r.getKey());

		// RFC 6605 Section 4
		BigInteger x = readBigInteger(in, keyinfo.length);
		BigInteger y = readBigInteger(in, keyinfo.length);
		ECPoint q = new ECPoint(x, y);

		KeyFactory factory = KeyFactory.getInstance("EC");
		return factory.generatePublic(new ECPublicKeySpec(q, keyinfo.spec));
	}

	/** Converts a KEY/DNSKEY record into a PublicKey */
	static PublicKey toPublicKey(KEYBase r) throws DNSSECException {
		int alg = r.getAlgorithm();
		try {
			switch (alg) {
			case Algorithm.RSAMD5:
			case Algorithm.RSASHA1:
			case Algorithm.RSA_NSEC3_SHA1:
			case Algorithm.RSASHA256:
			case Algorithm.RSASHA512:
				return toRSAPublicKey(r);
			case Algorithm.DSA:
			case Algorithm.DSA_NSEC3_SHA1:
				return toDSAPublicKey(r);
			case Algorithm.ECDSAP256SHA256:
				return toECDSAPublicKey(r, ECDSA_P256);
			case Algorithm.ECDSAP384SHA384:
				return toECDSAPublicKey(r, ECDSA_P384);
			default:
				throw new UnsupportedAlgorithmException(alg);
			}
		} catch (IOException e) {
			throw new MalformedKeyException(r);
		} catch (GeneralSecurityException e) {
			throw new DNSSECException(e.toString());
		}
	}

	private static byte[] fromRSAPublicKey(RSAPublicKey key) {
		DNSOutput out = new DNSOutput();
		BigInteger exponent = key.getPublicExponent();
		BigInteger modulus = key.getModulus();
		int exponentLength = BigIntegerLength(exponent);

		if (exponentLength < 256)
			out.writeU8(exponentLength);
		else {
			out.writeU8(0);
			out.writeU16(exponentLength);
		}
		writeBigInteger(out, exponent);
		writeBigInteger(out, modulus);

		return out.toByteArray();
	}

	private static byte[] fromDSAPublicKey(DSAPublicKey key) {
		DNSOutput out = new DNSOutput();
		BigInteger q = key.getParams().getQ();
		BigInteger p = key.getParams().getP();
		BigInteger g = key.getParams().getG();
		BigInteger y = key.getY();
		int t = (p.toByteArray().length - 64) / 8;

		out.writeU8(t);
		writeBigInteger(out, q);
		writeBigInteger(out, p);
		writeBigInteger(out, g);
		writeBigInteger(out, y);

		return out.toByteArray();
	}

	private static byte[] fromECDSAPublicKey(ECPublicKey key) {
		DNSOutput out = new DNSOutput();

		BigInteger x = key.getW().getAffineX();
		BigInteger y = key.getW().getAffineY();

		writeBigInteger(out, x);
		writeBigInteger(out, y);

		return out.toByteArray();
	}

	/** Builds a DNSKEY record from a PublicKey */
	static byte[] fromPublicKey(PublicKey key, int alg) throws DNSSECException {

		switch (alg) {
		case Algorithm.RSAMD5:
		case Algorithm.RSASHA1:
		case Algorithm.RSA_NSEC3_SHA1:
		case Algorithm.RSASHA256:
		case Algorithm.RSASHA512:
			if (!(key instanceof RSAPublicKey))
				throw new IncompatibleKeyException();
			return fromRSAPublicKey((RSAPublicKey) key);
		case Algorithm.DSA:
		case Algorithm.DSA_NSEC3_SHA1:
			if (!(key instanceof DSAPublicKey))
				throw new IncompatibleKeyException();
			return fromDSAPublicKey((DSAPublicKey) key);
		case Algorithm.ECDSAP256SHA256:
		case Algorithm.ECDSAP384SHA384:
			if (!(key instanceof ECPublicKey))
				throw new IncompatibleKeyException();
			return fromECDSAPublicKey((ECPublicKey) key);
		default:
			throw new UnsupportedAlgorithmException(alg);
		}
	}

	/**
	 * Convert an algorithm number to the corresponding JCA string.
	 * 
	 * @param alg
	 *            The algorithm number.
	 * @throws UnsupportedAlgorithmException
	 *             The algorithm is unknown.
	 */
	public static String algString(int alg)
			throws UnsupportedAlgorithmException {
		switch (alg) {
		case Algorithm.RSAMD5:
			return "MD5withRSA";
		case Algorithm.DSA:
		case Algorithm.DSA_NSEC3_SHA1:
			return "SHA1withDSA";
		case Algorithm.RSASHA1:
		case Algorithm.RSA_NSEC3_SHA1:
			return "SHA1withRSA";
		case Algorithm.RSASHA256:
			return "SHA256withRSA";
		case Algorithm.RSASHA512:
			return "SHA512withRSA";
		case Algorithm.ECDSAP256SHA256:
			return "SHA256withECDSA";
		case Algorithm.ECDSAP384SHA384:
			return "SHA384withECDSA";
		default:
			throw new UnsupportedAlgorithmException(alg);
		}
	}

	private static final int ASN1_SEQ = 0x30;
	private static final int ASN1_INT = 0x2;

	private static final int DSA_LEN = 20;

	@SuppressWarnings("unused")
	private static byte[] DSASignaturefromDNS(byte[] dns)
			throws DNSSECException, IOException {
		if (dns.length != 1 + DSA_LEN * 2)
			throw new SignatureVerificationException();

		DNSInput in = new DNSInput(dns);
		DNSOutput out = new DNSOutput();

		int t = in.readU8();

		byte[] r = in.readByteArray(DSA_LEN);
		int rlen = DSA_LEN;
		if (r[0] < 0)
			rlen++;

		byte[] s = in.readByteArray(DSA_LEN);
		int slen = DSA_LEN;
		if (s[0] < 0)
			slen++;

		out.writeU8(ASN1_SEQ);
		out.writeU8(rlen + slen + 4);

		out.writeU8(ASN1_INT);
		out.writeU8(rlen);
		if (rlen > DSA_LEN)
			out.writeU8(0);
		out.writeByteArray(r);

		out.writeU8(ASN1_INT);
		out.writeU8(slen);
		if (slen > DSA_LEN)
			out.writeU8(0);
		out.writeByteArray(s);

		return out.toByteArray();
	}

	@SuppressWarnings("unused")
	private static byte[] DSASignaturetoDNS(byte[] signature, int t)
			throws IOException {
		DNSInput in = new DNSInput(signature);
		DNSOutput out = new DNSOutput();

		out.writeU8(t);

		int tmp = in.readU8();
		if (tmp != ASN1_SEQ)
			throw new IOException();
		int seqlen = in.readU8();

		tmp = in.readU8();
		if (tmp != ASN1_INT)
			throw new IOException();
		int rlen = in.readU8();
		if (rlen == DSA_LEN + 1) {
			if (in.readU8() != 0)
				throw new IOException();
		} else if (rlen != DSA_LEN)
			throw new IOException();
		byte[] bytes = in.readByteArray(DSA_LEN);
		out.writeByteArray(bytes);

		tmp = in.readU8();
		if (tmp != ASN1_INT)
			throw new IOException();
		int slen = in.readU8();
		if (slen == DSA_LEN + 1) {
			if (in.readU8() != 0)
				throw new IOException();
		} else if (slen != DSA_LEN)
			throw new IOException();
		bytes = in.readByteArray(DSA_LEN);
		out.writeByteArray(bytes);

		return out.toByteArray();
	}

	private static byte[] ECDSASignaturefromDNS(byte[] signature,
			ECKeyInfo keyinfo) throws DNSSECException, IOException {
		if (signature.length != keyinfo.length * 2)
			throw new SignatureVerificationException();

		DNSInput in = new DNSInput(signature);
		DNSOutput out = new DNSOutput();

		byte[] r = in.readByteArray(keyinfo.length);
		int rlen = keyinfo.length;
		if (r[0] < 0)
			rlen++;

		byte[] s = in.readByteArray(keyinfo.length);
		int slen = keyinfo.length;
		if (s[0] < 0)
			slen++;

		out.writeU8(ASN1_SEQ);
		out.writeU8(rlen + slen + 4);

		out.writeU8(ASN1_INT);
		out.writeU8(rlen);
		if (rlen > keyinfo.length)
			out.writeU8(0);
		out.writeByteArray(r);

		out.writeU8(ASN1_INT);
		out.writeU8(slen);
		if (slen > keyinfo.length)
			out.writeU8(0);
		out.writeByteArray(s);

		return out.toByteArray();
	}

	@SuppressWarnings("unused")
	private static byte[] ECDSASignaturetoDNS(byte[] signature,
			ECKeyInfo keyinfo) throws IOException {
		DNSInput in = new DNSInput(signature);
		DNSOutput out = new DNSOutput();

		int tmp = in.readU8();
		if (tmp != ASN1_SEQ)
			throw new IOException();
		int seqlen = in.readU8();

		tmp = in.readU8();
		if (tmp != ASN1_INT)
			throw new IOException();
		int rlen = in.readU8();
		if (rlen == keyinfo.length + 1) {
			if (in.readU8() != 0)
				throw new IOException();
		} else if (rlen != keyinfo.length)
			throw new IOException();
		byte[] bytes = in.readByteArray(keyinfo.length);
		out.writeByteArray(bytes);

		tmp = in.readU8();
		if (tmp != ASN1_INT)
			throw new IOException();
		int slen = in.readU8();
		if (slen == keyinfo.length + 1) {
			if (in.readU8() != 0)
				throw new IOException();
		} else if (slen != keyinfo.length)
			throw new IOException();
		bytes = in.readByteArray(keyinfo.length);
		out.writeByteArray(bytes);

		return out.toByteArray();
	}

	private static void verify(PublicKey key, int alg, byte[] data,
			byte[] signature) throws DNSSECException {
		if (key instanceof DSAPublicKey) {
			try {
				signature = DSASignaturefromDNS(signature);
			} catch (IOException e) {
				throw new IllegalStateException();
			}
		} else if (key instanceof ECPublicKey) {
			try {
				switch (alg) {
				case Algorithm.ECDSAP256SHA256:
					signature = ECDSASignaturefromDNS(signature, ECDSA_P256);
					break;
				case Algorithm.ECDSAP384SHA384:
					signature = ECDSASignaturefromDNS(signature, ECDSA_P384);
					break;
				default:
					throw new UnsupportedAlgorithmException(alg);
				}
			} catch (IOException e) {
				throw new IllegalStateException();
			}
		}

		try {
			Signature s = Signature.getInstance(algString(alg));
			s.initVerify(key);
			s.update(data);
			if (!s.verify(signature))
				throw new SignatureVerificationException();
		} catch (GeneralSecurityException e) {
			throw new DNSSECException(e.toString());
		}
	}

	private static boolean matches(SIGBase sig, KEYBase key) {
		return (key.getAlgorithm() == sig.getAlgorithm()
				&& key.getFootprint() == sig.getFootprint() && key.getName()
				.equals(sig.getSigner()));
	}

	/**
	 * Verify a DNSSEC signature.
	 * 
	 * @param rrset
	 *            The data to be verified.
	 * @param rrsig
	 *            The RRSIG record containing the signature.
	 * @param key
	 *            The DNSKEY record to verify the signature with.
	 * @throws UnsupportedAlgorithmException
	 *             The algorithm is unknown
	 * @throws MalformedKeyException
	 *             The key is malformed
	 * @throws KeyMismatchException
	 *             The key and signature do not match
	 * @throws SignatureExpiredException
	 *             The signature has expired
	 * @throws SignatureNotYetValidException
	 *             The signature is not yet valid
	 * @throws SignatureVerificationException
	 *             The signature does not verify.
	 * @throws DNSSECException
	 *             Some other error occurred.
	 */
	public static void verify(RRset rrset, RRSIGRecord rrsig, DNSKEYRecord key)
			throws DNSSECException {
		if (!matches(rrsig, key))
			throw new KeyMismatchException(key, rrsig);

		Date now = new Date();
		if (now.compareTo(rrsig.getExpire()) > 0)
			throw new SignatureExpiredException(rrsig.getExpire(), now);
		if (now.compareTo(rrsig.getTimeSigned()) < 0)
			throw new SignatureNotYetValidException(rrsig.getTimeSigned(), now);

		verify(key.getPublicKey(), rrsig.getAlgorithm(),
				digestRRset(rrsig, rrset), rrsig.getSignature());
	}

	private static byte[] sign(PrivateKey privkey, PublicKey pubkey, int alg,
			byte[] data, String provider) throws DNSSECException {
		byte[] signature;
		try {
			Signature s;
			if (provider != null)
				s = Signature.getInstance(algString(alg), provider);
			else
				s = Signature.getInstance(algString(alg));
			s.initSign(privkey);
			s.update(data);
			signature = s.sign();
		} catch (GeneralSecurityException e) {
			throw new DNSSECException(e.toString());
		}

		if (pubkey instanceof DSAPublicKey) {
			try {
				DSAPublicKey dsa = (DSAPublicKey) pubkey;
				BigInteger P = dsa.getParams().getP();
				int t = (BigIntegerLength(P) - 64) / 8;
				signature = DSASignaturetoDNS(signature, t);
			} catch (IOException e) {
				throw new IllegalStateException();
			}
		} else if (pubkey instanceof ECPublicKey) {
			try {
				switch (alg) {
				case Algorithm.ECDSAP256SHA256:
					signature = ECDSASignaturetoDNS(signature, ECDSA_P256);
					break;
				case Algorithm.ECDSAP384SHA384:
					signature = ECDSASignaturetoDNS(signature, ECDSA_P384);
					break;
				default:
					throw new UnsupportedAlgorithmException(alg);
				}
			} catch (IOException e) {
				throw new IllegalStateException();
			}
		}

		return signature;
	}

	static void checkAlgorithm(PrivateKey key, int alg)
			throws UnsupportedAlgorithmException {
		switch (alg) {
		case Algorithm.RSAMD5:
		case Algorithm.RSASHA1:
		case Algorithm.RSA_NSEC3_SHA1:
		case Algorithm.RSASHA256:
		case Algorithm.RSASHA512:
			if (!(key instanceof RSAPrivateKey))
				throw new IncompatibleKeyException();
			break;
		case Algorithm.DSA:
		case Algorithm.DSA_NSEC3_SHA1:
			if (!(key instanceof DSAPrivateKey))
				throw new IncompatibleKeyException();
			break;
		case Algorithm.ECDSAP256SHA256:
		case Algorithm.ECDSAP384SHA384:
			if (!(key instanceof ECPrivateKey))
				throw new IncompatibleKeyException();
			break;
		default:
			throw new UnsupportedAlgorithmException(alg);
		}
	}

	/**
	 * Generate a DNSSEC signature. key and privateKey must refer to the same
	 * underlying cryptographic key.
	 * 
	 * @param rrset
	 *            The data to be signed
	 * @param key
	 *            The DNSKEY record to use as part of signing
	 * @param privkey
	 *            The PrivateKey to use when signing
	 * @param inception
	 *            The time at which the signatures should become valid
	 * @param expiration
	 *            The time at which the signatures should expire
	 * @throws UnsupportedAlgorithmException
	 *             The algorithm is unknown
	 * @throws MalformedKeyException
	 *             The key is malformed
	 * @throws DNSSECException
	 *             Some other error occurred.
	 * @return The generated signature
	 */
	public static RRSIGRecord sign(RRset rrset, DNSKEYRecord key,
			PrivateKey privkey, Date inception, Date expiration)
			throws DNSSECException {
		return sign(rrset, key, privkey, inception, expiration, null);
	}

	/**
	 * Generate a DNSSEC signature. key and privateKey must refer to the same
	 * underlying cryptographic key.
	 * 
	 * @param rrset
	 *            The data to be signed
	 * @param key
	 *            The DNSKEY record to use as part of signing
	 * @param privkey
	 *            The PrivateKey to use when signing
	 * @param inception
	 *            The time at which the signatures should become valid
	 * @param expiration
	 *            The time at which the signatures should expire
	 * @param provider
	 *            The name of the JCA provider. If non-null, it will be passed
	 *            to JCA getInstance() methods.
	 * @throws UnsupportedAlgorithmException
	 *             The algorithm is unknown
	 * @throws MalformedKeyException
	 *             The key is malformed
	 * @throws DNSSECException
	 *             Some other error occurred.
	 * @return The generated signature
	 */
	public static RRSIGRecord sign(RRset rrset, DNSKEYRecord key,
			PrivateKey privkey, Date inception, Date expiration, String provider)
			throws DNSSECException {
		int alg = key.getAlgorithm();
		checkAlgorithm(privkey, alg);

		RRSIGRecord rrsig = new RRSIGRecord(rrset.getName(), rrset.getDClass(),
				rrset.getTTL(), rrset.getType(), alg, rrset.getTTL(),
				expiration, inception, key.getFootprint(), key.getName(), null);

		rrsig.setSignature(sign(privkey, key.getPublicKey(), alg,
				digestRRset(rrsig, rrset), provider));
		return rrsig;
	}

	static SIGRecord signMessage(Message message, SIGRecord previous,
			KEYRecord key, PrivateKey privkey, Date inception, Date expiration)
			throws DNSSECException {
		int alg = key.getAlgorithm();
		checkAlgorithm(privkey, alg);

		SIGRecord sig = new SIGRecord(Name.root, DClass.ANY, 0, 0, alg, 0,
				expiration, inception, key.getFootprint(), key.getName(), null);
		DNSOutput out = new DNSOutput();
		digestSIG(out, sig);
		if (previous != null)
			out.writeByteArray(previous.getSignature());
		message.toWire(out);

		sig.setSignature(sign(privkey, key.getPublicKey(), alg,
				out.toByteArray(), null));
		return sig;
	}

	static void verifyMessage(Message message, byte[] bytes, SIGRecord sig,
			SIGRecord previous, KEYRecord key) throws DNSSECException {
		if (!matches(sig, key))
			throw new KeyMismatchException(key, sig);

		Date now = new Date();

		if (now.compareTo(sig.getExpire()) > 0)
			throw new SignatureExpiredException(sig.getExpire(), now);
		if (now.compareTo(sig.getTimeSigned()) < 0)
			throw new SignatureNotYetValidException(sig.getTimeSigned(), now);

		DNSOutput out = new DNSOutput();
		digestSIG(out, sig);
		if (previous != null)
			out.writeByteArray(previous.getSignature());

		Header header = (Header) message.getHeader().clone();
		header.decCount(Section.ADDITIONAL);
		out.writeByteArray(header.toWire());

		out.writeByteArray(bytes, Header.LENGTH, message.sig0start
				- Header.LENGTH);

		verify(key.getPublicKey(), sig.getAlgorithm(), out.toByteArray(),
				sig.getSignature());
	}

	/**
	 * Generate the digest value for a DS key
	 * 
	 * @param key
	 *            Which is covered by the DS record
	 * @param digestid
	 *            The type of digest
	 * @return The digest value as an array of bytes
	 */
	static byte[] generateDSDigest(DNSKEYRecord key, int digestid) {
		MessageDigest digest;
		try {
			switch (digestid) {
			case DSRecord.Digest.SHA1:
				digest = MessageDigest.getInstance("sha-1");
				break;
			case DSRecord.Digest.SHA256:
				digest = MessageDigest.getInstance("sha-256");
				break;
			case DSRecord.Digest.SHA384:
				digest = MessageDigest.getInstance("sha-384");
				break;
			default:
				throw new IllegalArgumentException("unknown DS digest type "
						+ digestid);
			}
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("no message digest support");
		}
		digest.update(key.getName().toWire());
		digest.update(key.rdataToWireCanonical());
		return digest.digest();
	}

}
